//
// Created by yangentao on 2021/2/1.
//

import Foundation
#if os(iOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

public extension View {

    var linearParams: LinearParams {
        get {
            ensureAttr(key: "linearParams") {
                LinearParams()
            }
        }
        set {
            setAttr("linearParams", newValue)
            if newValue.height == WrapContent || newValue.width == WrapContent {
                self.needLayoutOnPropChanged()
            }
        }
    }


    @discardableResult
    func linearParams(_ block: (LinearParams) -> Void) -> Self {
        let p = linearParams
        block(p)
        if p.height == WrapContent || p.width == WrapContent {
            self.needLayoutOnPropChanged()
        }
        return self
    }
}


public class LinearParams: Applyable {

    //weight指Cell的大小,
    //当weight == 0时, cell大小取决于width/height
    @GreatEQ(minValue: 0)
    public var weight: CGFloat = 0


    public var width: CGFloat = 0
    public var height: CGFloat = 0


    public var gravityX: GravityX = .none
    public var gravityY: GravityY = .none

    @GreatEQ(minValue: 0)
    public var minWidth: CGFloat = 0
    @GreatEQ(minValue: 0)
    public var minHeight: CGFloat = 0

    @GreatEQ(minValue: 0)
    public var maxWidth: CGFloat = 0
    @GreatEQ(minValue: 0)
    public var maxHeight: CGFloat = 0

    public var margins: Edge = Edge()

    public init() {

    }

}

public extension LinearParams {

    @discardableResult
    func widthWrap() -> Self {
        self.width = WrapContent
        return self
    }

    @discardableResult
    func heightWrap() -> Self {
        self.height = WrapContent
        return self
    }


    @discardableResult
    func widthMatch() -> Self {
        self.width = MatchParent
        return self
    }

    @discardableResult
    func heightMatch() -> Self {
        self.height = MatchParent
        return self
    }

    @discardableResult
    func sizeWrap() -> Self {
        self.width = WrapContent
        self.height = WrapContent
        return self
    }

}

public class LinearLayout: BaseLayout {
    public var axis: LayoutAxis = .vertical {
        didSet {
            postLayout()
        }
    }
    public var padding: Edge = Edge() {
        didSet {
            postLayout()
        }
    }
    //view gravity in cell
    public var defaultGravityX: GravityX = .fill {
        didSet {
            postLayout()
        }
    }
    //view gravity in cell
    public var defaultGravityY: GravityY = .fill {
        didSet {
            postLayout()
        }
    }

    public convenience init(_ axis: LayoutAxis) {
        self.init(frame: .zero)
        self.axis = axis
    }

    public func paddings(hor: CGFloat, ver: CGFloat) -> Self {
        padding = Edge.X(hor).ver(ver)
        return self
    }

    public func paddings(left: CGFloat, top: CGFloat, right: CGFloat, bottom: CGFloat) -> Self {
        padding = Edge(left: left, top: top, right: right, bottom: bottom)
        return self
    }

    override func layoutChildren(_ calcSize: CGSize?) -> CGSize {

        let viewList = self.subviews
        if viewList.count == 0 {
            return .zero
        }
        let tmpBounds = bounds
        var sz = calcSize ?? tmpBounds.size
        sz.width -= padding.horSpace
        sz.height -= padding.verSpace

        let cells: [LinearCell] = viewList.map {
            LinearCell($0)
        }
        if axis == .vertical {
            calcSizesVertical(sz, cells)
            let (maxX, maxY) = calcCellRectVer(cells)
            if calcSize == nil {
                layoutChildrenVertical(cells)
            }
            return CGSize(width: max(0, maxX - tmpBounds.minX), height: max(0, maxY - tmpBounds.minY))
        } else {
            calcSizesHor(sz, cells)
            let (maxX, maxY) = calcCellRectHor(cells)
            if calcSize == nil {
                layoutChildrenHor(cells)
            }
            return CGSize(width: max(0, maxX - tmpBounds.minX), height: max(0, maxY - tmpBounds.minY))
        }
    }

    //TODO paddings, margins
    private func calcCellFix(_ size: CGSize, cell: LinearCell) {
        let p = cell.param
        if p.width > 0 {
            cell.width = p.width
        }
        if p.height > 0 {
            cell.height = p.height
        }

        if p.width == WrapContent || p.height == WrapContent {
            var mSize: CGSize = size
            mSize.width -= cell.view.linearParams.margins.horSpace
            mSize.height -= cell.view.linearParams.margins.verSpace
            let sz = wrapSizeOf(view: cell.view, mSize)
            if p.width == WrapContent {
                cell.width = sz.width
            }
            if p.height == WrapContent {
                cell.height = sz.height
            }
        }
        if cell.width >= 0 {
            if p.minWidth > 0 {
                cell.width = max(p.minWidth, cell.width)
            }
            if p.maxWidth > 0 {
                cell.width = min(p.maxWidth, cell.width)
            }
        }
        if cell.height >= 0 {
            if p.minHeight > 0 {
                cell.height = max(p.minHeight, cell.height)
            }
            if p.maxHeight > 0 {
                cell.height = max(p.maxHeight, cell.height)
            }
        }


    }

    private func calcSizesVertical(_ size: CGSize, _ cells: [LinearCell]) {
        for cell in cells {
            calcCellFix(size, cell: cell)
            if cell.param.width == MatchParent {
                cell.width = max(0, size.width - cell.param.margins.horSpace)
            }
            if cell.width < 0 {
                fatalError("linear layout: width invalid")
            }
        }

        var weightSum: CGFloat = cells.sumBy { cell in
            cell.param.weight
        }
        var matchSum: Int = cells.sumBy { cell in
            cell.param.height == MatchParent ? 1 : 0
        }
        if weightSum > 0 && matchSum > 0 {
            fatalError("MatchParent and weight can NOT appear in same time  ")
        }
        var avaliableHeight = size.height - cells.sumBy { cell in
            max(0, cell.height) + cell.param.margins.verSpace
        }


        if matchSum > 0 {
            let ls = cells.filter {
                $0.param.height == MatchParent
            }
            for cell in ls {
                let p = cell.param
                let matchValue: CGFloat = max(0, avaliableHeight) / matchSum
                cell.height = max(0, p.minHeight, matchValue)
                if p.maxHeight > 0 {
                    cell.height = min(p.maxHeight, cell.height)
                }
                matchSum -= 1
                avaliableHeight = max(0, avaliableHeight - cell.height)
            }
        }
        if weightSum > 0 {
            let ls = cells.filter {
                $0.param.weight > 0
            }
            for cell in ls {
                let p = cell.param
                let value: CGFloat = max(0, avaliableHeight) / weightSum
                cell.height = max(0, p.minHeight, value)
                if p.maxHeight > 0 {
                    cell.height = min(p.maxHeight, cell.height)
                }
                weightSum -= p.weight
                avaliableHeight = max(0, avaliableHeight - cell.height)
            }
        }

    }

    private func calcSizesHor(_ size: CGSize, _ cells: [LinearCell]) {
        for cell in cells {
            calcCellFix(size, cell: cell)
            if cell.param.height == MatchParent {
                cell.height = max(0, size.height - cell.param.margins.verSpace)
            }
            if cell.height < 0 {
                fatalError("linear layout: height invalid")
            }
        }

        var weightSum: CGFloat = cells.sumBy { cell in
            cell.param.weight
        }
        var matchSum: Int = cells.sumBy { cell in
            cell.param.width == MatchParent ? 1 : 0
        }
        if weightSum > 0 && matchSum > 0 {
            fatalError("MatchParent and weight can NOT appear in same time  ")
        }
        var avaliableWidth = size.width - cells.sumBy { cell in
            max(0, cell.width) + cell.param.margins.horSpace
        }


        if matchSum > 0 {
            let ls = cells.filter {
                $0.param.width == MatchParent
            }
            for cell in ls {
                let p = cell.param
                let matchValue: CGFloat = max(0, avaliableWidth) / matchSum
                cell.width = max(0, p.minWidth, matchValue)
                if p.maxWidth > 0 {
                    cell.width = min(p.maxWidth, cell.width)
                }
                matchSum -= 1
                avaliableWidth = max(0, avaliableWidth - cell.width)
            }
        }
        if weightSum > 0 {
            let ls = cells.filter {
                $0.param.weight > 0
            }
            for cell in ls {
                let p = cell.param
                let value: CGFloat = max(0, avaliableWidth) / weightSum
                cell.width = max(0, p.minWidth, value)
                if p.maxWidth > 0 {
                    cell.width = min(p.maxWidth, cell.width)
                }
                weightSum -= p.weight
                avaliableWidth = max(0, avaliableWidth - cell.width)
            }
        }
    }

    private func calcCellRectVer(_ cells: [LinearCell]) -> (CGFloat, CGFloat) {
        var maxX: CGFloat = 0
        for cell in cells {
            let p = cell.param
            var gX = p.gravityX
            if gX == .none {
                gX = defaultGravityX
            }
            switch gX {
            case .none, .left:
                cell.x = bounds.minX + padding.left + p.margins.left
            case .right:
                cell.x = bounds.maxX - padding.right - p.margins.right - cell.width
            case .center:
                cell.x = bounds.minX + padding.left + p.margins.left + max(0, (bounds.width - padding.horSpace - p.margins.horSpace - cell.width) / 2)
            case .fill:
                cell.x = bounds.minX + padding.left + p.margins.left
                let space = bounds.width - padding.horSpace - p.margins.horSpace
                cell.width = max(space, cell.width)
            }
            maxX = max(maxX, cell.x + cell.width + cell.param.margins.right + padding.right)
        }
        var fromY = bounds.minY + padding.top
        for cell in cells {
            fromY += cell.param.margins.top
            cell.y = fromY
            cell.height = max(0, cell.height)
            fromY += cell.height + cell.param.margins.bottom
        }
        let maxY: CGFloat = fromY + padding.bottom
        return (maxX, maxY)
    }

    private func calcCellRectHor(_ cells: [LinearCell]) -> (CGFloat, CGFloat) {
        var maxY: CGFloat = 0
        for cell in cells {
            let p = cell.param
            var gY = p.gravityY
            if gY == .none {
                gY = defaultGravityY
            }
            switch gY {
            case .none, .top:
                cell.y = bounds.minY + padding.top + p.margins.top
            case .bottom:
                cell.y = bounds.maxY - padding.bottom - p.margins.bottom - cell.height
            case .center:
                cell.y = bounds.minY + padding.top + p.margins.top + max(0, (bounds.height - padding.verSpace - p.margins.verSpace - cell.height) / 2)
            case .fill:
                cell.y = bounds.minY + padding.top + p.margins.top
                let space = bounds.height - padding.verSpace - p.margins.verSpace
                cell.height = max(space, cell.height)
            }
            maxY = max(maxY, cell.y + cell.height + cell.param.margins.bottom + padding.bottom)
        }
        var fromX = bounds.minX + padding.left
        for cell in cells {
            fromX += cell.param.margins.left
            cell.x = fromX
            cell.width = max(0, cell.width)
            fromX += cell.width + cell.param.margins.right
        }
        let maxX: CGFloat = fromX + padding.right
        return (maxX, maxY)
    }

    private func layoutChildrenHor(_ cells: [LinearCell]) {
        for cell in cells {
            let r = cell.rect
            if cell.view.frame != r {
                cell.view.constraintStorage.updateFrame(r)
            }
        }
    }

    private func layoutChildrenVertical(_ cells: [LinearCell]) {
        for cell in cells {
            let r = cell.rect
            if cell.view.frame != r {
                cell.view.constraintStorage.updateFrame(r)
            }
        }
    }
}

fileprivate let LINEAR_UNSPEC: CGFloat = -1000

fileprivate class LinearCell {
    var view: View
    lazy var param: LinearParams = view.linearParams
    var x: CGFloat = LINEAR_UNSPEC
    var y: CGFloat = LINEAR_UNSPEC
    var width: CGFloat = LINEAR_UNSPEC
    var height: CGFloat = LINEAR_UNSPEC

    var wrapSize: Size = Size(width: 0, height: 0)

    init(_ view: View) {
        self.view = view
    }

    var rect: Rect {
        return Rect(x: x, y: y, width: width, height: height)
    }
}

